﻿using System.Reflection;
using System.Threading;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using XcoAppSpaces.Core.Ports;

namespace test.XcoAppSpaces.Core.Ports
{
	[TestFixture]
    public class PortTests
    {
		[Test]
		public void PostAndReceive()
		{
			var port = new Port<int>();
			port.Post(123);

			var result = port.Receive().Result;

			Assert.That(result, Is.EqualTo(123));
		}

		[Test]
		public void PostAndReceiveDelayed()
		{
			var port = new Port<int>();

			Task.Delay(500).ContinueWith(_ =>
				{
					port.Post(123);
					Console.WriteLine("posted");
				});

			Console.WriteLine("receiving");
			var result = port.Receive().Result;
			Console.WriteLine("received");

			Assert.That(result, Is.EqualTo(123));
		}

		[Test]
		public void Receive_with_timeout()
		{
			//Arrange
			var port = new Port<int>();

			try
			{
				//Act
				var result = port.Receive(100).Result;
			}
			catch (AggregateException ex)
			{
				//Assert
				Assert.That(ex.InnerExceptions.First(), Is.InstanceOf<TimeoutException>());
			}
		}

		[Test]
		public void After_timeout_the_next_item_is_received_correctly()
		{
			//Arrange
			var port = new Port<int>();
			var timedOutTask = port.Receive(100);
			Thread.Sleep(200);

			//Act
			port.Post(123);
			var result = port.Receive(100).Result;

			//Assert
			Assert.That(result, Is.EqualTo(123));
		}

		[Test]
		public void ProcessSequentially()
		{
			//Arrange
			var port = new Port<int>();
			var sequence = new List<string>();
			port.RegisterReceiver(async m =>
				{
					sequence.Add("start" + m);
					//Console.WriteLine("start " + m);
					await Task.Delay(250);
					sequence.Add("end" + m);
					//Console.WriteLine("end " + m);
				}, 1);

			//Act
			port.Post(1);
			port.Post(2);
			port.Post(3);

			//Assert
			Assert.IsTrue(SpinWait.SpinUntil(() => sequence.Count == 6, 2000));
			Assert.That(sequence, Is.EqualTo(new[] { "start1", "end1", "start2", "end2", "start3", "end3" }));
		}

		[Test]
		public void ProcessConcurrently()
		{
			//Arrange
			var port = new Port<int>();
			var sequence = new List<string>();
			port.RegisterReceiver(m =>
			{
				sequence.Add("start" + m);
				//Console.WriteLine("start " + m);
				Thread.Sleep(500);
				sequence.Add("end" + m);
				//Console.WriteLine("end " + m);
			}, 4);

			//Act
			port.Post(1);
			port.Post(2);
			port.Post(3);

			//Assert
			Assert.IsTrue(SpinWait.SpinUntil(() => sequence.Count == 6, 2000));
			Assert.That(sequence, Is.Not.EqualTo(new[] { "start1", "end1", "start2", "end2", "start3", "end3" }));
		}

		[Test]
		public void PostAndAwaitCompletion()
		{
			//Arrange
			var port = new Port<int>();
			port.RegisterReceiver(async m =>
			{
				Console.WriteLine("start " + m);
				await Task.Delay(500);
				Console.WriteLine("end " + m);
			}, 1);
			
			//Act & Assert
			var completion = port.PostAndAwaitSent(1);

			Assert.IsFalse(completion.IsCompleted);

			//Console.WriteLine("waiting for completion");
			completion.Wait(1000);
			Assert.IsTrue(completion.IsCompleted);

			//Console.WriteLine("completed");
		}

		[Test]
		public void ProcessWithExceptionUncaught()
		{
			var port = new Port<int>();
			port.RegisterReceiver(m =>
			{
				throw new Exception("error " + m);
			}, 4);
			port.Post(1);
			port.Post(2);
			port.Post(3);

			Thread.Sleep(1000);

			Assert.That(CausalityContext.UnhandledExceptionPort.ItemCount, Is.EqualTo(3));
		}

		[Test]
		public void ProcessWithExceptionCausality_Explicit()
		{
			var port = new Port<int>();
			var errorPort = new Port<Exception>();
			port.RegisterReceiver(m =>
			{
				throw new Exception("error " + m);
			}, 4);
			port.Post(1, errorPort);
			port.Post(2, errorPort);
			port.Post(3, errorPort);

			Thread.Sleep(1000);

			Assert.That(errorPort.ItemCount, Is.EqualTo(3));
			Assert.That(CausalityContext.UnhandledExceptionPort.ItemCount, Is.EqualTo(0));
		}

		[Test]
		public void ProcessWithExceptionCausality_Implicit()
		{
			var port = new Port<int>();
			var errorPort = new Port<Exception>();
			port.RegisterReceiver(m =>
			{
				throw new Exception("error " + m);
			}, 4);
			CausalityContext.Add(new Causality(errorPort));
			port.Post(1);
			port.Post(2);
			port.Post(3);
			CausalityContext.Clear();
			
			Thread.Sleep(1000);

			Assert.That(errorPort.ItemCount, Is.EqualTo(3));
			Assert.That(CausalityContext.UnhandledExceptionPort.ItemCount, Is.EqualTo(0));
		}

		[Test]
		public void ProcessWithExceptionCausality_Cascaded_Ports()
		{
			var port2 = new Port<int>(m => { throw new Exception("error " + m); });
			var port1 = new Port<int>(m => port2.Post(m));
			var errorPort = new Port<Exception>();
			
			CausalityContext.Add(new Causality(errorPort));
			port1.Post(123);
			CausalityContext.Clear();

			Thread.Sleep(1000);

			Assert.That(errorPort.ItemCount, Is.EqualTo(1));
			Assert.That(CausalityContext.UnhandledExceptionPort.ItemCount, Is.EqualTo(0));
		}

		[Test]
		public void UnregisterReceiver()
		{
			//Arrange
			var port = new Port<int>();
			bool processedByReceiver = false;
			port.RegisterReceiver(m => { processedByReceiver = true; });

			//Act
			port.UnregisterReceiver();
			port.Post(1);

			//Assert
			Assert.IsFalse(SpinWait.SpinUntil(() => processedByReceiver, 500));
			Assert.That(port.Receive().Result, Is.EqualTo(1));
		}

		private string delegateVar;
		private void ActionDelegate(string msg)
		{
			delegateVar = msg;
		}
		private async Task FuncDelegate(string msg)
		{
			await Task.Delay(1);
			delegateVar = msg;
		}

		[Test]
		public void RegisterReceiverDelegate_Action()
		{
			//Arrange
			delegateVar = null;
			var method = this.GetType().GetMethod("ActionDelegate", BindingFlags.Instance | BindingFlags.NonPublic);
			var methodDelegate = Delegate.CreateDelegate(typeof(Action<string>), this, method);
			var port = new Port<string>();

			//Act
			port.RegisterUnknownTypeReceiver(methodDelegate);
			port.Post("test");

			//Assert
			Assert.IsTrue(SpinWait.SpinUntil(() => delegateVar == "test", 1000));

		}

		[Test]
		public void RegisterReceiverDelegate_Func()
		{
			//Arrange
			delegateVar = null;
			var method = this.GetType().GetMethod("FuncDelegate", BindingFlags.Instance | BindingFlags.NonPublic);
			var methodDelegate = Delegate.CreateDelegate(typeof(Func<string,Task>), this, method);
			var port = new Port<string>();

			//Act
			port.RegisterUnknownTypeReceiver(methodDelegate);
			port.Post("test");

			//Assert
			Assert.IsTrue(SpinWait.SpinUntil(() => delegateVar == "test", 1000));

		}

		[Test]
		[ExpectedException(typeof(ArgumentException))]
		public void RegisterReceiverDelegate_Invalid_Action()
		{
			//Arrange
			delegateVar = null;
			var method = this.GetType().GetMethod("ActionDelegate", BindingFlags.Instance | BindingFlags.NonPublic);
			var methodDelegate = Delegate.CreateDelegate(typeof(Action<string>), this, method);
			var port = new Port<int>();

			//Act
			port.RegisterUnknownTypeReceiver(methodDelegate);
		}
    }
}
